﻿namespace Proto
{
    using System;
    using System.Collections.Generic;
    using System.Threading;
    using Proto.Fields;
    using Proto.Providers;
    using Proto.Util;

    public interface IRequiresService
    {
        void RecieveProcessing();
        event Action<IRequiresService> RequestService;
    }

    public delegate void ChangePending<in T>(T addition );
    public delegate void Changed<in T>(T removal );

    public class Sub<T> : ISub<T> where T : class, IFieldGetter
    {
        internal static int REF_COUNT = 0;
        ~Sub()
        {
            REF_COUNT--;
        }

        private readonly IKey m_SubscriptionKey;
        private Dictionary<IKey, SubItem<T>> m_ItemMap;
        private Queue<UpdateItem> m_PendingUpdates;
        private Queue<UpdateItem> m_PendingUpdatesStage;
        private DateTime m_LastUpdate;

        // cached copy of dictionary used during processing
        private readonly Dictionary<IKey, T> m_NewStateCache = new Dictionary<IKey, T>(1000);

        // cached copy of dictionary used during processing
        private readonly Dictionary<IKey, T> m_RemoveSet = new Dictionary<IKey, T>(100);

        public Sub( IKey subscription_key ) : this( subscription_key, null){}

        public Sub( IKey subscription_key, Runner auto_register )
        {
            REF_COUNT++;
            m_SubscriptionKey = subscription_key;
            m_ItemMap = new Dictionary<IKey, SubItem<T>>( 1000 );
            m_PendingUpdates = new Queue<UpdateItem>( 1000 );
            m_PendingUpdatesStage = new Queue<UpdateItem>(1000);
            
            if( auto_register != null ) auto_register.Register( this );
        }

        public void Die()
        {
            // because of threading concerns we route this back onto app thread.
            Action action = new Action(
                delegate
                    {
                        RequestService = delegate { };

                        foreach (SubItem<T> sub in m_ItemMap.Values)
                        {
                            sub.Die();
                        }

                        m_ItemMap = null;
                        m_PendingUpdates = null;
                        m_PendingUpdatesStage = null;
                    });

            RunnerRegistry.APP.Enqueue(action);
        }

        public IKey SubscriptionKey
        {
            get { return this.m_SubscriptionKey; }
        }

        public SubState CurrentState
        {
            get
            {
                if( m_LastUpdate == default(DateTime) ) return SubState.Uninitialized;
                if( m_PendingUpdates.Count == 1 ) return SubState.UpdatePending;
                if (m_PendingUpdates.Count > 1 ) return SubState.InBatch;
                if( m_ItemMap.Count == 0 ) return SubState.Uninitialized;
                return SubState.Ready;
            }
        }


        public void RecieveProcessing()
        {
            // TODO: Refactor
            if ( ( m_PendingUpdates == null || m_PendingUpdates.Count == 0 )
                 &&
                 ( m_PendingUpdatesStage == null || m_PendingUpdatesStage.Count == 0 )
                ) return;

            
            Queue<UpdateItem> working_set = m_PendingUpdatesStage;

            Dictionary<IKey, T> new_state = m_NewStateCache;
            Dictionary<IKey, T> remove_set = m_RemoveSet;
            bool has_clear = false;

            while( working_set.Count > 0 )
            {
                UpdateItem update = working_set.Dequeue();
                if (update.UpdateType == UpdateType.Add || update.UpdateType == UpdateType.Update)
                {
                    new_state[update.Item.Key] = update.Item;
                }
                else if (update.UpdateType == UpdateType.Remove)
                {
                    remove_set[update.Item.Key] = update.Item;
                }
                else if (update.UpdateType == UpdateType.Clear)
                {
                    has_clear = true;
                    new_state.Clear();
                    remove_set.Clear();
                }
            }
            // end state should be nulls in owning set where removals should happen.
            // values where add or change should happen.

            if( has_clear )
            {
                NotifyClearPending();
            }
            if (remove_set != null)
            {
                foreach (T item in remove_set.Values)
                {
                    if (m_ItemMap.ContainsKey(item.Key))
                    {
                        HandleRemove(item);
                    }
                }
            }
            foreach( T item in new_state.Values )
            {
                this.HandleAddOrUpdate( item );
            }

            m_PendingUpdatesStage = m_PendingUpdates;
            m_PendingUpdates = working_set;
            m_NewStateCache.Clear();
            m_RemoveSet.Clear();

            // because we need to assure that PendingUpdatesStage recieves processing
            if (m_PendingUpdatesStage.Count > 0 && RequestService != null )
                RequestService(this);
        }

        private void HandleAddOrUpdate( T item )
        {
            // TODO: check proper thread / batch process

            SubItem<T> sub;
            Dictionary<IKey, SubItem<T>> local_map = m_ItemMap;
            if (local_map == null) return;

            if (local_map.TryGetValue(item.Key, out sub) == false)
            {
                sub = new SubItem<T>( item.Key );
                if (m_NotifyAdditionPending != null)
                    m_NotifyAdditionPending(sub);

                local_map.Add(item.Key, sub);
            }

            sub.EnqueueUpdate(item);
            sub.ProcessOutstandingUpdates();
        }

        private void HandleRemove( T item )
        {
            SubItem<T> sub;
            if( m_ItemMap.TryGetValue( item.Key, out sub ) == false )
            {
#if DEBUG
                throw new ArgumentException( "Unable remove item: " + item.Key );
#endif
            }
            else
            {
                NotifyRemovalPending(sub);
                m_ItemMap.Remove(item.Key);
            }
        }

        public void EnqueueRemoval(T update)
        {
            m_LastUpdate = QuickTime.Now;
            if (m_PendingUpdates == null) return;
            m_PendingUpdates.Enqueue(new UpdateItem(update, UpdateType.Remove));
            
            RequestService(this);
        }

        public void EnqueueAddition(T update)
        {
            m_LastUpdate = QuickTime.Now;

            if (m_PendingUpdates == null) return;
            m_PendingUpdates.Enqueue(new UpdateItem(update, UpdateType.Add));
            
            RequestService(this);
        }

        public void EnqueueBatchAddition(IEnumerable<T> update_list)
        {
            m_LastUpdate = QuickTime.Now;
            foreach (T item in update_list)
            {
                if (m_PendingUpdates == null) return;
                m_PendingUpdates.Enqueue(new UpdateItem(item, UpdateType.Add));
            }

            RequestService(this);
        }

        public void EnqueueBatchUpdate(IEnumerable<T> update_list)
        {

            m_LastUpdate = QuickTime.Now;
            foreach (T item in update_list)
            {
                if (m_PendingUpdates == null) return;
                m_PendingUpdates.Enqueue(new UpdateItem(item, UpdateType.Update));
            }

            RequestService(this);
        }

        public void EnqueueUpdate(T update)
        {
            m_LastUpdate = QuickTime.Now;
            if (m_PendingUpdates == null) return;
            m_PendingUpdates.Enqueue( new UpdateItem(update, UpdateType.Update ));
            
            RequestService(this);
        }

        public void EnqueueBatchRemoval(IEnumerable<T> update_list)
        {
            m_LastUpdate = QuickTime.Now;
            
            foreach (T item in update_list)
            {
                m_PendingUpdates.Enqueue( new UpdateItem( item, UpdateType.Remove ) );
            }

            RequestService(this);
        }

        public void EnqueueClear()
        {
            m_LastUpdate = QuickTime.Now;
            if (m_PendingUpdates == null) return;
            m_PendingUpdates.Enqueue(new UpdateItem(null, UpdateType.Clear));
            
            RequestService(this);
        }


        /// <summary> 
        /// Solves the issue of loading initial state "Mid Stream" by handing off to APP.
        /// </summary>
        private void InitializeRequestor(ChangePending<SubItem<T>> addition_delegate)
        {
            Action action = new Action(
                delegate
                {
#if DEBUG
                        if( Thread.CurrentThread != RunnerRegistry.APP.Thread )
                        {
                            throw new ApplicationException();
                        }
#endif
                    foreach (SubItem<T> item in m_ItemMap.Values)
                    {
                        addition_delegate(item);
                    }
                    m_NotifyAdditionPending += addition_delegate;
                }
                    );

            RunnerRegistry.APP.Enqueue(action);
        }

        private event ChangePending<SubItem<T>> m_NotifyAdditionPending;
        public event ChangePending<SubItem<T>> NotifyAdditionPending
        {
            add
            {
                this.InitializeRequestor( value );
            }
            remove
            {
                m_NotifyAdditionPending -= value;
            }
        }

        public event ChangePending<SubItem<T>> NotifyRemovalPending = delegate { };
        public event Action NotifyClearPending = delegate { };
        public event Action<IRequiresService> RequestService = delegate { };
        
        private enum UpdateType : short
        {
            Add, Remove, Update, Clear
        }

        private struct UpdateItem
        {
            public readonly UpdateType UpdateType;
            public readonly T Item;

            public UpdateItem(T item, UpdateType type)
            {
                if( item == null && type != UpdateType.Clear )throw new ArgumentException( "Item cannot be null unless type is Clear");
                Item = item;
                UpdateType = type;
            }
        }
    }
}